ES6 使用了“一个 JavaScript ”的方式来避免版本化的问题。

那么,什么是“版本化”?什么又是“一个 JavaScript”呢?

版本化

一般地,版本化就是说一门语言分成了不同的版本,新版本可以清理老版本中不好的特性,或者改变某些特性的运作方式。这就会导致新的代码无法在老引擎中运行,老的代码也不能在新引擎中运行。很可能某些代码就只能在特定版本的引擎中正常运行,然后针对不同版本的引擎,就要写不同的代码。

如果代码库升级到新的语言版本,就有两种处理方式。

第一种,彻底升级代码库中所有的代码。但是如果代码库的代码量很大的话,就很坑爹了。

第二种,让代码库包含多个语言版本的代码,根据指定的语言版本使用不同的执行引擎。对于 ES6 ,就可以使用媒体类型来标记 ES6 代码,比如在 HTTP 响应头中设置:

1
Content-Type: application/ecmascript;version=6

也可以利用 <script> 标签的 type 属性来标记:

1
2
3
<script type="application/ecmascript;version=6">
···
</script>

也可以在代码内部标记版本(类似于 'use strict' ,放在 JavaScript 文件第一行):

1
use version 6;

这两种类型的标记方式都有问题:外部版本标记法很脆弱,容易丢失;内部版本标记法又会使代码显得杂乱。

一个更根本的问题是,针对不同的语言版本,要维护不同的执行引擎。这就产生了几个问题:

  • 引擎变得很臃肿,因为要实现所有版本的语法。对于语言分析工具也带来了同样的问题(比如类型检测, JSLint )。
  • 开发者需要记住版本之间的不同点。
  • 代码变得更加难以重构,因为在移动代码的时候需要考虑语言版本的问题。

因此,应该避免版本化,尤其是 JavaScript 和 web 。

一个 JavaScript

既然版本化有这么多弊端,对于 JavaScript 和 web 来说都不适用,那么如何避免版本化呢?

采用向后兼容的方式。这就是说我们必须放弃一些关于清理 JavaScript 语言的野心:不能引入破坏性的改变。向后兼容就是不移除已有特性,也不改变已有特性。该规则的口号就是:“不要破坏 web 代码”。

我们可以增加新的特性,使已有的特性更加强大。

这样一来,新的语言和引擎就不需要版本号了,因为仍然需要能够运行老的代码。 David Herman 称这种避免版本化的方式为“一个 JavaScript ”,它避免了 JavaScript 被拆分成不同的版本或者模式。甚至,“一个 JavaScript ”纠正了之前由于严格模式引入的 JavaScript 分支。

“一个 Javascript ”并不是说就要完全放弃对语言的清理。相对于去掉已有的特性,可以引入新的干净的特性。 let 就是这样干的,它用于声明块级变量,是 var 的改进版。但是它并没有替换掉 var ,只是作为更好的方案与 var 并存。

将来某个时候,可能会清除掉不再有人使用的特性。实际上,一些 ES6 特性是通过调查 web 上的代码来设计的,比如下面两个:

  • let 声明很难引入到非严格模式中,因为在非严格模式下 let 并不是保留字。在 ES5 中,有且仅有一种形式的 let 变量是合法的:
1
let[x] = arr;

调查发现, web 上没人会在非严格模式下这样使用 let 变量,这就使得 TC39 能够将 let 引入非严格模式中。

严格模式和 ES6

ECMAScript 5 引入严格模式来对语言进行清理。在文件或者函数的第一行放入下面的内容就可以打开严格模式:

1
'use strict';

严格模式带来了三种具有破坏性的改变:

  • 语法改变:一些之前合法的语法在严格模式下面是不允许的。例如:
    • 禁止 with 语句。它允许开发者添加任何对象到作用域链,这会减缓程序的执行速度,并且很难指出某个变量指向哪里。
    • 删除一个独立的标识符(是一个变量,而不是一个属性)是不允许的。
    • 函数只能在作用域的顶层声明。
    • 更多的保留字: implements interface let package private protected public static yield 。
  • 更多类型的错误。例如:
    • 给一个未声明的变量赋值会抛出 ReferenceError 。而在非严格模式下,这样做就会创建一个全局变量。
    • 修改只读的属性(比如字符串的长度属性)会抛出 TypeError 。而在非严格模式下,不会产生任何效果。
  • 不同的语义:在严格模式下,一些结构体会表现得不一样。例如:
    • arguments 不再随着当前参数值的改变而改变。
    • 在非方法的函数中 thisundefined 。在非严格模式下,它指向全局对象( window )。如果调用一个构造器的时候没有使用 new ,就会创建一些全局变量。

从严格模式的这些破坏性改变中可以看出,版本化是很棘手的:即便能够制定出一个干净版本的 JavaScript ,也很难被大家接受。主要原因在于会破坏很多现有的代码,会减缓执行速度,并且引入到文件很繁琐(更不用说交互式的命令行)。

支持松散(非严格)模式

一个 JavaScript 意味着我们不能放弃松散模式:此模式将会继续存在(例如在 HTML 属性中)。因此,我们不能基于严格模式来构建 ECMAScript 6 ,必须同时在严格模式和非严格模式(又称为松散模式)中都增加相同的特性。否则,严格模式就会成为语言的一个不同版本,回到了版本化的方式。

但是很不幸,有两个特性很难引入松散模式: let 声明和块级函数声明。让我们看看为什么很难引入和如何引入。

松散模式中的 let 声明

let 使你能够声明块级变量。这很难被引入到松散模式,因为 let 仅在严格模式下是保留字。也就是说,下面两条语句在 ES5 的松散模式下是合法的:

1
2
var let = [];
let[x] = 'abc';

在 ECMASCript 6 的严格模式下,第一行就会抛出异常。因为使用了 let 作为变量名。然后第二行会被解析为一个 let 变量声明(使用解构)。

在 ECMAScript 6 的松散模式下,第一行不会抛出异常,但是第二行依然被解析为一个 let 声明。这种使用 let 的方式在 web 上是极少见的,因此 ES6 可以直接这样来解析。 ES5 松散模式下的其他 let 声明的书写方式不会被误解:

1
2
let foo = 123;
let {x,y} = computeCoordinates();

松散模式下的块级函数声明

ECMAScript 5 严格模式中,是禁止在块中声明函数的;在松散模式下,却可以这么做,但是没说这样做会发生什么。因此,很多 JavaScript 实现都支持块级函数声明,但是处理方式是不一样的。

ECMAScript 6 想要块中的函数声明本地化(即该函数的作用域就在该块中)。作为 ES5 严格模式的升级,这是没问题的,但是会破坏一些松散模式的代码。因此, ES6 为浏览器提供了“ web 遗留的兼容语义”,允许块中的函数声明在函数作用域范围内存在。

其它关键字

标识符 yieldstatic 仅在 ES5 的严格模式下是保留字。 ECMAScript 6 使用上下文相关的语法规则来使它们在松散模式下起作用:

  • 在松散模式下, yield 仅在生成器函数中是保留字。
  • static 现在仅用于类字面量中,类字面中默认就是严格模式的(见下文)。

隐式的严格模式

在 ECMAScript 6 中,模块体和类体默认就是严格模式的–没必要使用 use strict 标记。考虑到将来所有的代码都会位于模块中, ECMAScript 6 有效地将整个语言升级到了严格模式。

其它语法结构(比如箭头函数和生成器函数)本来也应该隐式地为严格模式,但是考虑到通常情况下这些结构都很小,在非严格模式下使用它们就会造成代码中两种模式的碎片化切换。类,尤其是模块一般是足够大的,这样一来就可以忽略两种模式的碎片化切换问题了。

无法修复的东西

一个 JavaScript的缺陷就是无法修复已有的怪异行为,尤其是下面这两个。

第一个, typeof null 应该返回字符串 null 而不是 object ,修正这个就会破坏已有的代码。而另一方面,给新类型的操作数定义新的操作结果是没问题的, ECMAScript 6 的 Symbol 就是一个例子:

1
2
> typeof Symbol.iterator
'symbol'

第二个,全局对象(浏览器中的 window 对象)不应该在变量作用域链,现在修正这个也太晚了。但是至少,在模块中不会直接处于全局作用域下,并且 let 永远不会创建全局对象属性,甚至在全局作用域下使用也不会。

总结

一个 JavaScript意思就是使 ECMAScript 6 完全地向后兼容,很高兴这获得了成功。尤其是模块隐式就是严格模式的(这样一来我们大部分的代码都会处于严格模式下)。

在短期内,对于制定 ES6 规范和引擎实现来说,给严格模式和松散模式添加 ES6 的语法结构会耗费更多的精力。从长远来看,规范和引擎将会受益于语言不分叉(更少的膨胀等等)。开发人员会立即从一个 JavaScript 中获得好处,因为开始使用 ECMAScript 6 变得更加容易。